#include <audio/MIDI/MIDI.h>
#include <mathematics/Math.h>
#include <system/Log.h>

namespace ne
{
    MIDISequence::MIDISequence(ResourceManager *pManager,
                               const uint64_t id,
                               const std::string &name,
                               const std::string &fileName,
                               const bool isManual,
                               ManualResourceLoader *pLoader)
        : Resource(pManager, id, name, fileName, isManual, pLoader),
          mBPM(MIDI_DEFAULT_BPM),
          mMPQN(MIDI_DEFAULT_MPQN),
          mTPQN(MIDI_DEFAULT_TPQN),
          mFPS(0),
          mTPF(0),
          mLastEventType(0xFF)
    {
    }

    void MIDISequence::setBPM(const uint32_t BPM)
    {
        if (BPM > 0)
        {
            mBPM = BPM;
            mMPQN = MIDI_MICROSECONDS_PER_MINUTE / mBPM;
        }
    }

    uint32_t MIDISequence::getBPM() const
    {
        return mBPM;
    }

    void MIDISequence::setMPQN(const uint32_t MPQN)
    {
        if (MPQN > 0)
        {
            mMPQN = MPQN;
            mBPM = MIDI_MICROSECONDS_PER_MINUTE / mMPQN;
        }
    }

    uint32_t MIDISequence::getMPQN() const
    {
        return mMPQN;
    }

    void MIDISequence::setTPQN(const uint32_t TPQN)
    {
        mTPQN = TPQN;
    }

    uint32_t MIDISequence::getTPQN() const
    {
        return mTPQN;
    }

    double MIDISequence::time2Ticks(const double time) const
    {
        if (mFPS > 0 && mTPF > 0)
        {
            return (time * mFPS * mTPF) / MIDI_MICROSECONDS_PER_SECOND;
        }

        return (time * mTPQN) / mMPQN;
    }

    double MIDISequence::ticks2Time(const double ticks) const
    {
        if (mFPS > 0 && mTPF > 0)
        {
            return (ticks * MIDI_MICROSECONDS_PER_SECOND) / (mFPS * mTPF);
        }

        return (ticks * mMPQN) / mTPQN;
    }

    void MIDISequence::decodeEvent(const byte *buffer, uint32_t &offset)
    {
        // TODO: Get DeltaTime
        uint32_t deltaTime = decodeVariableLengthValue(buffer, offset);
        Log::Out("MIDI DeltaTime=%d", deltaTime);

        // TODO: Get Event
        byte event = buffer[offset++];
        // TODO: For same type events
        if (event < MIDI_DATA_FLAG)
        {
            event = mLastEventType;
            --offset;
        }

        if (event < MIDI_SYSTEM)
        {
            MIDIEvent *pEvent = decodeChannelEvent(event, buffer, offset);
            pEvent->relatedTicks = deltaTime;
            mpLastTrack->pushBackEvent(pEvent);
            Log::Out("MIDI AbsoluteTicks=%lf", pEvent->absoluteTicks);
        }
        else if (event == MIDI_META)
        {
            decodeMetaEvent(event, buffer, offset);
        }
        else
        {
            decodeSysExEvent(event, buffer, offset);
        }
        mLastEventType = event;
    }

    MIDIEvent* MIDISequence::decodeChannelEvent(const byte event, const byte *buffer, uint32_t &offset)
    {
        byte cmd = event & MIDI_EVENT_MASK;
        byte channel = event & MIDI_CHANNEL_MASK;
        MIDIChannelEvent *pEvent = new MIDIChannelEvent(cmd, channel);
        switch (cmd)
        {
            case MIDI_NOTE_OFF:
            {
                byte note = buffer[offset++] & MIDI_DATA_MASK;
                byte velocity = buffer[offset++] & MIDI_DATA_MASK;
                pEvent->parameter1 = note;
                pEvent->parameter2 = velocity;
                Log::Out("MIDI Channel Event: Note Off[%d, %d, %d]", channel, note, velocity);
                break;
            }

            case MIDI_NOTE_ON:
            {
                byte note = buffer[offset++] & MIDI_DATA_MASK;
                byte velocity = buffer[offset++] & MIDI_DATA_MASK;
                pEvent->parameter1 = note;
                pEvent->parameter2 = velocity;
                Log::Out("MIDI Channel Event: Note On[%d, %d, %d]", channel, note, velocity);
                break;
            }

            case MIDI_NOTE_PRESSURE:
            {
                byte note = buffer[offset++] & MIDI_DATA_MASK;
                byte pressure = buffer[offset++] & MIDI_DATA_MASK;
                pEvent->parameter1 = note;
                pEvent->parameter2 = pressure;
                Log::Out("MIDI Channel Event: Note Pressure[%d, %d, %d]", channel, note, pressure);
                break;
            }

            case MIDI_CONTROLLER:
            {
                byte controller = buffer[offset++] & MIDI_DATA_MASK;
                byte value = buffer[offset++] & MIDI_DATA_MASK;
                pEvent->parameter1 = controller;
                pEvent->parameter2 = value;
                Log::Out("MIDI Channel Event: Controller[%d, %d, %d]", channel, controller, value);
                break;
            }

            case MIDI_INSTRUMENT:
            {
                byte instrument = buffer[offset++] & MIDI_DATA_MASK;
                pEvent->parameter1 = instrument;
                mpLastTrack->setInstrument(instrument);
                Log::Out("MIDI Channel Event: Instrument[%d, %d]", channel, instrument);
                break;
            }

            case MIDI_CHANNEL_PRESSURE:
            {
                byte pressure = buffer[offset++] & MIDI_DATA_MASK;
                pEvent->parameter1 = pressure;
                Log::Out("MIDI Channel Event: Channel Pressure[%d, %d]", channel, pressure);
                break;
            }

            case MIDI_PITCH_BEND:
            {
                byte valueLSB = buffer[offset++] & MIDI_DATA_MASK;
                byte valueMSB = buffer[offset++] & MIDI_DATA_MASK;
                uint32_t value = (valueMSB << 7) + valueLSB;
                pEvent->parameter1 = valueLSB;
                pEvent->parameter2 = valueMSB;
                Log::Out("MIDI Channel Event: Pitch Bend[%d, %d]", channel, value);
                break;
            }

            default:
            {
                Log::Out(LL_WARNING,
                    "Decode Error: MIDI Channel Event[0x%X]", event);
                break;
            }
        }
        return pEvent;
    }

    void MIDISequence::decodeMetaEvent(const byte event, const byte *buffer, uint32_t &offset)
    {
        byte meta = buffer[offset++];
        uint32_t length = decodeVariableLengthValue(buffer, offset);
        switch (meta)
        {
            case MIDI_META_SEQUENCE_NUMBER:
            {
                byte numberMSB = buffer[offset++] & MIDI_DATA_MASK;
                byte numberLSB = buffer[offset++] & MIDI_DATA_MASK;
                Log::Out("MIDI Meta Event: Sequence Number[%d, %d]", numberMSB, numberLSB);
                break;
            }

            case MIDI_META_TEXT:
            {
                std::string text = decodeString(buffer, offset, length);
                Log::Out("MIDI Meta Event: Text[%s]", text.c_str());
                break;
            }

            case MIDI_META_COPYRIGHT_NOTICE:
            {
                std::string text = decodeString(buffer, offset, length);
                Log::Out("MIDI Meta Event: Copyright Notice[%s]", text.c_str());
                break;
            }

            case MIDI_META_TRACK_NAME:
            {
                std::string text = decodeString(buffer, offset, length);
                Log::Out("MIDI Meta Event: Track Name[%s]", text.c_str());
                break;
            }

            case MIDI_META_INSTRUMENT_NAME:
            {
                std::string text = decodeString(buffer, offset, length);
                Log::Out("MIDI Meta Event: Instrument Name[%s]", text.c_str());
                break;
            }

            case MIDI_META_LYRICS:
            {
                std::string text = decodeString(buffer, offset, length);
                Log::Out("MIDI Meta Event: Lyrics[%s]", text.c_str());
                break;
            }

            case MIDI_META_MARKER:
            {
                std::string text = decodeString(buffer, offset, length);
                Log::Out("MIDI Meta Event: Marker[%s]", text.c_str());
                break;
            }

            case MIDI_META_CUE_POINT:
            {
                std::string text = decodeString(buffer, offset, length);
                Log::Out("MIDI Meta Event: Cue Point[%s]", text.c_str());
                break;
            }

            case MIDI_META_PROGRAM_NAME:
            {
                std::string text = decodeString(buffer, offset, length);
                Log::Out("MIDI Meta Event: Program Name[%s]", text.c_str());
                break;
            }

            case MIDI_META_DEVICE_NAME:
            {
                std::string text = decodeString(buffer, offset, length);
                Log::Out("MIDI Meta Event: Device Name[%s]", text.c_str());
                break;
            }

            case MIDI_META_CHANNEL_PREFIX:
            {
                byte channel = buffer[offset++] & MIDI_DATA_MASK;
                Log::Out("MIDI Meta Event: Channel Prefix[%d]", channel);
                break;
            }

            case MIDI_META_END_OF_TRACK:
            {
                Log::Out("MIDI Meta Event: End Of Track");
                break;
            }

            case MIDI_META_SET_TEMPO:
            {
                uint32_t MPQN = buffer[offset++] & MIDI_DATA_MASK;
                MPQN = (MPQN << 8) + (buffer[offset++] & MIDI_DATA_MASK);
                MPQN = (MPQN << 8) + (buffer[offset++] & MIDI_DATA_MASK);
                setMPQN(MPQN);
                Log::Out("MIDI Meta Event: Set Tempo MPQN[%d]", MPQN);
                break;
            }

            case MIDI_META_SMPTE_OFFSET:
            {
                byte hour = buffer[offset++] & MIDI_DATA_MASK;
                byte minute = buffer[offset++] & MIDI_DATA_MASK;
                byte second = buffer[offset++] & MIDI_DATA_MASK;
                byte frame = buffer[offset++] & MIDI_DATA_MASK;
                byte subframe = buffer[offset++] & MIDI_DATA_MASK;
                Log::Out("MIDI Meta Event: SMPTE Offset[%d, %d, %d, %d, %d]", hour, minute, second, frame, subframe);
                break;
            }

            case MIDI_META_TIME_SIGNATURE:
            {
                byte number = buffer[offset++] & MIDI_DATA_MASK; // pow(2, number)
                byte denominator = buffer[offset++] & MIDI_DATA_MASK;
                byte metronome = buffer[offset++] & MIDI_DATA_MASK;
                byte n32nds = buffer[offset++] & MIDI_DATA_MASK; // Specifies the number of 32nd notes per 24 MIDI clock signals
                Log::Out("MIDI Meta Event: Time Signature[%d, %d, %d, %d]", number, denominator, metronome, n32nds);
                break;
            }

            case MIDI_META_KEY_SIGNATURE:
            {
                byte key = buffer[offset++] & MIDI_DATA_MASK;
                byte scale = buffer[offset++] & MIDI_DATA_MASK;
                Log::Out("MIDI Meta Event: Key Signature[%d, %d]", key, scale);
                break;
            }

            case MIDI_META_SEQUENCER_SPECIFIC:
            {
                std::string text = decodeString(buffer, offset, length);
                Log::Out("MIDI Meta Event: Sequencer Specific[%s]", text.c_str());
                break;
            }

            default:
            {
                std::string text = decodeString(buffer, offset, length);
                Log::Out(LL_WARNING,
                    "Decode Error: MIDI Meta Event[0x%X: %s]", meta, text.c_str());
                break;
            }
        }
    }

    void MIDISequence::decodeSysExEvent(const byte event, const byte *buffer, uint32_t &offset)
    {
        switch (event)
        {
            case MIDI_SYSTEM:
            {
                uint32_t length = decodeVariableLengthValue(buffer, offset);
                std::string text = decodeString(buffer, offset, length);
                Log::Out("MIDI SysEx Event: System[%s]", text.c_str());
                break;
            }

            case MIDI_TIME_CODE:
            {
                byte value = buffer[offset++] & MIDI_DATA_MASK;
                Log::Out("MIDI SysEx Event: Time Code[%d]", value);
                break;
            }

            case MIDI_SONG_POSITION:
            {
                byte valueLSB = buffer[offset++] & MIDI_DATA_MASK;
                byte valueMSB = buffer[offset++] & MIDI_DATA_MASK;
                uint32_t value = (valueMSB << 7) + valueLSB;
                Log::Out("MIDI SysEx Event: Song Position[%d]", value);
                break;
            }

            case MIDI_SONG_SELECT:
            {
                byte value = buffer[offset++] & MIDI_DATA_MASK;
                Log::Out("MIDI SysEx Event: Song Select[%d]", value);
                break;
            }

            case MIDI_TUNE_REQUEST:
            {
                Log::Out("MIDI SysEx Event: Request");
                break;
            }

            case MIDI_END_OF_EXCLUSIVE:
            {
                Log::Out("MIDI SysEx Event: End Of Exclusive");
                break;
            }

            case MIDI_CLOCK:
            {
                Log::Out("MIDI SysEx Event: Clock");
                break;
            }

            case MIDI_START:
            {
                Log::Out("MIDI SysEx Event: Start");
                break;
            }

            case MIDI_CONTINUE:
            {
                Log::Out("MIDI SysEx Event: Continue");
                break;
            }

            case MIDI_ACTIVE_SENSE:
            {
                Log::Out("MIDI SysEx Event: Active Sense");
                break;
            }

            default:
            {
                Log::Out(LL_WARNING,
                    "Decode Error: MIDI SysEx Event[0x%X]", event);
                break;
            }
        }
    }

    uint32_t MIDISequence::decodeVariableLengthValue(const byte *buffer, uint32_t &offset)
    {
        uint32_t value = 0;
        do
        {
            value = (value << 7) + (buffer[offset++] & MIDI_DATA_MASK);
        }
        while (buffer[offset-1] & MIDI_DATA_FLAG);
        return value;
    }

    std::string MIDISequence::decodeString(const byte *buffer, uint32_t &offset, const uint32_t length)
    {
        std::string text((const char *)(buffer + offset), length);
        offset += length;
        return text;
    }

    size_t MIDISequence::getNumTracks() const
    {
        return mTrackVector.size();
    }

    MIDITrack* MIDISequence::getTrack(const size_t index) const
    {
        if (index < mTrackVector.size())
        {
            return mTrackVector[index];
        }
        return 0;
    }

    void MIDISequence::destroyAllTracks()
    {
        for (MIDITrackVectorIterator it=mTrackVector.begin(); it!=mTrackVector.end(); ++it)
        {
            delete (*it);
        }
        mTrackVector.clear();
    }

    MIDISequence::~MIDISequence()
    {
        unload();
    }

    bool MIDISequence::loadImplement()
    {
        FILE *file = openFile("rb");
        if (!file)
        {
            Log::Out(LL_WARNING,
                "Open MIDI (%s) file (%s) failed", getName().c_str(), getFileName().c_str());
            return false;
        }

        MIDIHeaderChunk midiHeader;
        if (!fread(&midiHeader, sizeof(byte) * 14, 1, file))
        {
            closeFile(file);
            Log::Out(LL_WARNING,
                "Load Header for MIDI (%s) from file (%s) failed", getName().c_str(), getFileName().c_str());
            return false;
        }
#if NE_ENDIAN == NE_ENDIAN_LITTLE
        midiHeader.mSize = Math::Endian(midiHeader.mSize);
        midiHeader.mFormat = Math::Endian(midiHeader.mFormat);
        midiHeader.mNumTracks = Math::Endian(midiHeader.mNumTracks);
        midiHeader.mTimeDivision = Math::Endian(midiHeader.mTimeDivision);
#endif

        Log::Out("============================================================");
        Log::Out("MIDI Header:");
        Log::Out("MIDI Size=%d", midiHeader.mSize);
        Log::Out("MIDI Format=%d", midiHeader.mFormat);
        Log::Out("MIDI NumTracks=%d", midiHeader.mNumTracks);
        Log::Out("MIDI TimeDivision=%d", midiHeader.mTimeDivision);

        if (midiHeader.mID[0] != MIDI_HEADER_ID[0] ||
            midiHeader.mID[1] != MIDI_HEADER_ID[1] ||
            midiHeader.mID[2] != MIDI_HEADER_ID[2] ||
            midiHeader.mID[3] != MIDI_HEADER_ID[3])
        {
            closeFile(file);
            Log::Out(LL_WARNING,
                "Load Header for MIDI (%s) from file (%s) with wrong ChunkID", getName().c_str(), getFileName().c_str());
            return false;
        }

        if (midiHeader.mTimeDivision & MIDI_TIME_DIVISION_FLAG)
        {
            mFPS = (midiHeader.mTimeDivision >> 8) & MIDI_DATA_MASK;
            mTPF = midiHeader.mTimeDivision & 0xFF;
            Log::Out("MIDI FPS=%d", mFPS);
            Log::Out("MIDI TPF=%d", mTPF);
        }
        else
        {
            mTPQN = midiHeader.mTimeDivision & MIDI_TIME_DIVISION_MASK;
            Log::Out("MIDI TPQN=%d", mTPQN);
        }

        MIDITrackChunk midiTrack;
        for (uint32_t i=0; i<midiHeader.mNumTracks; ++i)
        {
            if (!fread(&midiTrack, sizeof(byte) * 8, 1, file))
            {
                closeFile(file);
                Log::Out(LL_WARNING,
                    "Load Track for MIDI (%s) from file (%s) failed", getName().c_str(), getFileName().c_str());
                return false;
            }
#if NE_ENDIAN == NE_ENDIAN_LITTLE
            midiTrack.mSize = Math::Endian(midiTrack.mSize);
#endif

            Log::Out("============================================================");
            Log::Out("MIDI Track:");
            Log::Out("MIDI Size=%d", midiTrack.mSize);

            if (midiTrack.mID[0] != MIDI_TRACK_ID[0] ||
                midiTrack.mID[1] != MIDI_TRACK_ID[1] ||
                midiTrack.mID[2] != MIDI_TRACK_ID[2] ||
                midiTrack.mID[3] != MIDI_TRACK_ID[3])
            {
                closeFile(file);
                Log::Out(LL_WARNING,
                    "Load Track for MIDI (%s) from file (%s) with wrong ChunkID", getName().c_str(), getFileName().c_str());
                return false;
            }

            if (midiTrack.mSize > 0)
            {
                mpLastTrack = new MIDITrack();
                mTrackVector.push_back(mpLastTrack);
                byte buffer[midiTrack.mSize];
                if (!fread(buffer, sizeof(byte) * midiTrack.mSize, 1, file))
                {
                    closeFile(file);
                    Log::Out(LL_WARNING,
                        "Load Event data for MIDI (%s) from file (%s) failed", getName().c_str(), getFileName().c_str());
                    return false;
                }

                mLastEventType = 0xFF;
                uint32_t offset = 0;
                while (offset < midiTrack.mSize)
                {
                    decodeEvent(buffer, offset);
                }
                mpLastTrack = 0;
            }
        }
        Log::Out("============================================================");

        closeFile(file);

        return true;
    }

    bool MIDISequence::unloadImplement()
    {
        destroyAllTracks();
        return true;
    }

    bool MIDISequence::downloadImplement()
    {
        return true;
    }

    size_t MIDISequence::_calculateSize() const
    {
        return mSize;
    }
}
